package de.sendorian.app.forumArchive.download;

import static org.apache.commons.lang.StringUtils.isEmpty;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.log4j.Logger;
import org.hamcrest.Matcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import de.sendorian.app.forumArchive.domain.Archive;
import de.sendorian.app.forumArchive.domain.Forum;
import de.sendorian.app.forumArchive.domain.Image;
import de.sendorian.app.forumArchive.domain.Link;
import de.sendorian.app.forumArchive.domain.Post;
import de.sendorian.app.forumArchive.domain.Topic;
import de.sendorian.app.forumArchive.matcher.FileBlackilistMatcher;
import de.sendorian.app.forumArchive.renamer.FileRenamer;
import de.sendorian.app.forumArchive.repository.ForumRepository;
import de.sendorian.util.io.FileWriter;


@Component
public class Downloader implements Runnable {

    private static final Logger LOG = Logger.getLogger(Downloader.class);

    FileRenamer renamer;
    Matcher<Post> postMatcher;
    Matcher<Topic> topicMatcher;
    private @Value("#{props['threads_download']}") int threads;
    private @Value("#{props['download_dir']}") String downloadDir;
    private ExecutorService threadPool;
    private FileBlackilistMatcher linkBlacklist;

    private ForumRepository forumRepository;

    public Downloader() {
        linkBlacklist = new FileBlackilistMatcher("file:external_links_ignores.properties");
    }
    
    @PostConstruct
    public void init() {
        threadPool = Executors.newFixedThreadPool(threads);
    }

    @Override
    public void run() {
        downloadForums(forumRepository.findAll());
    }


    public void downloadForums(List<Forum> forums) {
        for (Forum forum : forums) {
            downloadForum(forum);
        }
    }


    public void downloadForum(Forum forum) {
        List<TopicInfos> topicInfos = new ArrayList<TopicInfos>();
        for (Topic topic : forum.getTopics()) {
            if (topicMatcher.matches(topic)) {
                topicInfos.add(downloadPosts(topic, forum.getName()));
            }
        }

        waitForDownloadsToComplete();

        Set<Link> externalLinks = new TreeSet<Link>();
        String downloadedFilesInfo = "";
        for (TopicInfos topicInfo : topicInfos) {
            if (!topicInfo.getDownloadedFiles().isEmpty()) {
                saveTextFile(topicInfo.getTopic().toStringFormatted(), topicInfo.getPath(), "Posts.txt");
                downloadedFilesInfo += topicInfo.getDownloadedFilesString();
            }
            externalLinks.addAll(topicInfo.getLinks());
        }

        String forumDir = downloadDir + forum.getName();
        String dateString = DateFormatUtils.format(new Date(), "yyyy_MM_dd");
        if (!isEmpty(downloadedFilesInfo)) {
            saveTextFile(downloadedFilesInfo, forumDir, "downloaded files " + dateString + ".txt");
        }

        String externalLinksString = getExternalLinksString(externalLinks);
        if (!isEmpty(externalLinksString)) {
            saveTextFile(externalLinksString, forumDir, "external links " + dateString + ".txt");
        }
    }


    /**
     * Downloads all Posts of the given topic that match the Downloaders post
     * matcher.
     */
    public TopicInfos downloadPosts(Topic topic, String forumName) {
        String forumPath = downloadDir + forumName;
        String path = forumPath + java.io.File.separator + topic.getName();
        Set<Link> links = new TreeSet<Link>();
        Set<String> downloadedFiles = new TreeSet<String>();

        for (Post post : topic.getPosts()) {
            if (!postMatcher.matches(post)) {
                continue;
            }

            List<String> filesInArchives = new ArrayList<String>();
            if (!post.getArchives().isEmpty()) {
                for (Archive archive : post.getArchives()) {
                    archive.setLocalPath(path);
                    ArchiveDownloadTask archiveDownloader = new ArchiveDownloadTask(archive, topic,
                            downloadedFiles, filesInArchives);
                    threadPool.execute(archiveDownloader);
                }
            }
            if (!post.getImages().isEmpty()) {
                for (Image image : post.getImages()) {
                    // for backwards compatibility only. Since 1.0_beta3 the PostFinder will do this.
                    // TODO delete when new database structure is final and database is created this way
                    renamer.rename(image);
                    image.setLocalPath(path);
                    ImageDownloadTask imageDownloader = new ImageDownloadTask(image, topic, downloadedFiles,
                            filesInArchives);
                    threadPool.execute(imageDownloader);
                }
            }

            links.addAll(post.getLinks());
        }

        return new TopicInfos(topic, path, downloadedFiles, links);
    }


    /**
     * Waits until all download tasks have been executed.
     */
    private void waitForDownloadsToComplete() {
        LOG.info("All download tasks have been initiated. Waiting for tasks to complete.");
        threadPool.shutdown();
        try {
            threadPool.awaitTermination(90 * 60, TimeUnit.SECONDS);
            LOG.info("All download tasks have been completed.");
        } catch (InterruptedException e) {
            LOG.error("Downloader was interrupted!", e);
        }
    }


    /**
     * Filters the links for external links, removes blacklisted links and sorts
     * the entries. It then returns a String containing all these links, one per
     * line.
     * 
     * @return a String containing one entry per line.
     */
    public String getExternalLinksString(Set<Link> links) {
        Set<String> externalLinks = new TreeSet<String>();
        for (Link link : links) {
            if (link.isExternal() && !linkBlacklist.matches(link.getTarget())) {
                externalLinks.add(link.getTarget());
            }
        }
        return createTopicInfoList(externalLinks, null);
    }


    static String createTopicInfoList(Set<? extends Object> infosToSave, String title) {
        if (infosToSave.isEmpty())
            return "";

        StringBuilder builder = new StringBuilder();
        if (title != null) {
            builder.append(title + "\n");
        }
        for (Object info : infosToSave) {
            builder.append(info.toString()).append('\n');
        }
        builder.append("\n\n");
        return builder.toString();
    }


    private static void saveTextFile(String fileContent, String forumPath, String fileName) {
        try {
            FileWriter fileWriter = new FileWriter();
            fileWriter.deleteFile(forumPath, fileName);
            fileWriter.write(fileContent.getBytes(), forumPath, fileName);
        } catch (IOException e) {
            LOG.error("Could not write forum info file \"" + fileName + "\"", e);
        }
    }


    @Value("#{config.defaultRenamer}")
    public void setRenamer(FileRenamer renamer) {
        this.renamer = renamer;
    }


    @Value("#{config.postMatcher}")
    public void setPostMatcher(Matcher<Post> postMatcher) {
        this.postMatcher = postMatcher;
    }


    @Value("#{config.topicMatcher}")
    public void setTopicMatcher(Matcher<Topic> topicMatcher) {
        this.topicMatcher = topicMatcher;
    }


    @Autowired
    public void setForumRepository(ForumRepository forumRepository) {
        this.forumRepository = forumRepository;
    }
}
